import {
  ActionCallback,
  Command,
  CommandType,
  DataSet,
  HistoryChangeCallback, ICreateNodeData,
  INodeData,
  ModifyNodeParam, SelectNodeOptions
} from "./interfaces.ts";
import {Stage} from "./Stage.ts";
import {NodeWrapper} from "./NodeWrapper.tsx";

export class CommandManager {
  protected _stage: Stage | undefined
  protected _actionCallback?: ActionCallback
  protected _historyChangeCallback?: HistoryChangeCallback

  protected get ctrl() {
    return this._stage!.ctrl!
  }

  protected get stage() {
    return this._stage!
  }

  init(
    stage: Stage,
    callbacks: {
      actionCallback?: ActionCallback,
      historyChangeCallback?: HistoryChangeCallback,
    }) {
    this._stage = stage
    this._actionCallback = callbacks.actionCallback
    this._historyChangeCallback = callbacks.historyChangeCallback
  }

  modifyNodes(modifyData: ModifyNodeParam[], oldSelections: number[]) {
    if (modifyData.length == 0) {
      return
    }

    const oldData: DataSet[] = []
    for (const actionItem of modifyData) {
      const data: DataSet = {}
      oldData.push(data)
      const keys = actionItem.data ? Object.keys(actionItem.data) : []
      for (const key of keys) {
        data[key] = getData(actionItem.oldData ?? actionItem.node.nodeData, key)
      }
    }

    return this._wrapCommand(
      CommandType.MODIFY_NODES,
      (firstly) => {
        if (firstly) {
          return
        }
        const selections = []
        for (let i = 0; i < modifyData.length; i++) {
          const actionItem = modifyData[i]
          if (actionItem.data) {
            const keys = Object.keys(actionItem.data)
            for (const key of keys) {
              setData(modifyData[i].node, key, actionItem.data[key])
            }
          }
          actionItem.node.$applyBounds()
          selections.push(actionItem.node.id)
        }
        this.ctrl.$updateGlobalBounds()
        this.ctrl.$setSelections(selections, true, false)
      },
      () => {
        for (let i = 0; i < modifyData.length; i++) {
          const actionItem = modifyData[i]
          if (actionItem.data) {
            const keys = Object.keys(actionItem.data)
            for (const key of keys) {
              setData(modifyData[i].node, key, oldData[i][key])
            }
          }
          actionItem.node.$applyBounds()
        }
        this.ctrl.$updateGlobalBounds()
        this.ctrl.$setSelections(oldSelections, true, false)
      }
    )
  }

  selectNode(node: NodeWrapper, options: SelectNodeOptions) {
    this._stage?.ctrl?.selectNode(node, options)
  }

  setSelections(selections: number[], oldSelections: number[]) {
    return this._wrapCommand(
      CommandType.SELECT_NODES,
      (firstly) => {
        if (firstly) {
          return
        }
        this.ctrl.$setSelections(selections, true, false)
      },
      () => {
        this.ctrl.$setSelections(oldSelections, true, false)
      },
    )
  }

  addNode(nodeData: INodeData, parentNodeId?: number, insertIndex?: number) {
    const {stage} = this
    let node: NodeWrapper

    return this._wrapCommand(
      CommandType.ADD_NODE,
      (firstly) => {
        node = stage.$addNode(nodeData as ICreateNodeData, parentNodeId, insertIndex)
      },
      () => {
        if (node) {
          stage.$removeNode(node.id)
        }
      }
    )
  }

  removeNode(nodeId: number) {
    const {stage, ctrl} = this
    let result: ReturnType<typeof stage.$removeNode>
    let selected: boolean

    return this._wrapCommand(
      CommandType.REMOVE_NODE,
      (firstly) => {
        selected = ctrl.selections.includes(nodeId)
        result = stage.$removeNode(nodeId)
      },
      () => {
        if (result) {
          stage.$addNode(result.nodeData as ICreateNodeData, result.parentNodeId, result.insertIndex)
          if (selected) {
            ctrl.$setSelections([nodeId], true, false)
          }
        }
      }
    )
  }

  protected _wrapCommand(type: CommandType, redo: (firstly?: boolean) => void, undo: () => void) {
    const command = {
      type,
      redo: () => {
        redo()
        this._actionCallback?.(type, 'redo')
      },
      undo: () => {
        undo()
        this._actionCallback?.(type, 'undo')
      },
    } as Command

    redo(true)
    this._historyChangeCallback?.(command)

    return command
  }
}

//todo 待替换
function getData(target: any, keyPath: string) {
  return new Function('target', `with (target) {return ${keyPath}}`)(target)
}

//todo 待替换
function setData(target: any, keyPath: string, data: any) {
  return new Function('target', '__data__', `with (target) {${keyPath} = __data__}`)(target, data)
}
